/**
 * @author Filipe Caixeta / http://filipecaixeta.com.br
 * @author Mugen87 / https://github.com/Mugen87
 *
 * Description: A THREE loader for PCD ascii and binary files.
 */

import Cookies from 'js-cookie'
import {
  BufferGeometry,
  FileLoader,
  Float32BufferAttribute,
  Loader,
  LoaderUtils,
  Points,
  PointsMaterial,
  LineBasicMaterial,
  LineSegments,
  Group,
} from 'three'
import { colorRender } from './PCDColorRender.js'

var PCDLoader = function (manager) {
  Loader.call(this, manager)

  this.littleEndian = true
}

PCDLoader.prototype = Object.assign(Object.create(Loader.prototype), {
  constructor: PCDLoader,

  load: function (url, onLoad, onProgress, onError) {
    var scope = this

    var loader = new FileLoader(scope.manager)
    loader.setPath(scope.path)
    loader.setResponseType('arraybuffer')
    loader.load(
      url,
      function (data) {
        try {
          onLoad(scope.parse(data, url))
        } catch (e) {
          if (onError) {
            onError(e)
          } else {
            throw e
          }
        }
      },
      onProgress,
      onError
    )
  },

  parse: function (data, url) {
    // from https://gitlab.com/taketwo/three-pcd-loader/blob/master/decompress-lzf.js

    function decompressLZF(inData, outLength) {
      var inLength = inData.length
      var outData = new Uint8Array(outLength)
      var inPtr = 0
      var outPtr = 0
      var ctrl
      var len
      var ref
      do {
        ctrl = inData[inPtr++]
        if (ctrl < 1 << 5) {
          ctrl++
          if (outPtr + ctrl > outLength) throw new Error('Output buffer is not large enough')
          if (inPtr + ctrl > inLength) throw new Error('Invalid compressed data')
          do {
            outData[outPtr++] = inData[inPtr++]
          } while (--ctrl)
        } else {
          len = ctrl >> 5
          ref = outPtr - ((ctrl & 0x1f) << 8) - 1
          if (inPtr >= inLength) throw new Error('Invalid compressed data')
          if (len === 7) {
            len += inData[inPtr++]
            if (inPtr >= inLength) throw new Error('Invalid compressed data')
          }

          ref -= inData[inPtr++]
          if (outPtr + len + 2 > outLength) throw new Error('Output buffer is not large enough')
          if (ref < 0) throw new Error('Invalid compressed data')
          if (ref >= outPtr) throw new Error('Invalid compressed data')
          do {
            outData[outPtr++] = outData[ref++]
          } while (--len + 2)
        }
      } while (inPtr < inLength)

      return outData
    }

    function parseHeader(data) {
      var PCDheader = {}
      var result1 = data.search(/[\r\n]DATA\s(\S*)\s/i)
      var result2 = /[\r\n]DATA\s(\S*)\s/i.exec(data.substr(result1 - 1))

      PCDheader.data = result2[1]
      PCDheader.headerLen = result2[0].length + result1
      PCDheader.str = data.substr(0, PCDheader.headerLen)

      // remove comments

      PCDheader.str = PCDheader.str.replace(/\#.*/gi, '')

      // parse

      PCDheader.version = /VERSION (.*)/i.exec(PCDheader.str)
      PCDheader.fields = /FIELDS (.*)/i.exec(PCDheader.str)
      PCDheader.size = /SIZE (.*)/i.exec(PCDheader.str)
      PCDheader.type = /TYPE (.*)/i.exec(PCDheader.str)
      PCDheader.count = /COUNT (.*)/i.exec(PCDheader.str)
      PCDheader.width = /WIDTH (.*)/i.exec(PCDheader.str)
      PCDheader.height = /HEIGHT (.*)/i.exec(PCDheader.str)
      PCDheader.viewpoint = /VIEWPOINT (.*)/i.exec(PCDheader.str)
      PCDheader.points = /POINTS (.*)/i.exec(PCDheader.str)

      // evaluate

      if (PCDheader.version !== null) PCDheader.version = parseFloat(PCDheader.version[1])

      if (PCDheader.fields !== null) PCDheader.fields = PCDheader.fields[1].split(' ')

      if (PCDheader.type !== null) PCDheader.type = PCDheader.type[1].split(' ')

      if (PCDheader.width !== null) PCDheader.width = parseInt(PCDheader.width[1])

      if (PCDheader.height !== null) PCDheader.height = parseInt(PCDheader.height[1])

      if (PCDheader.viewpoint !== null) PCDheader.viewpoint = PCDheader.viewpoint[1]

      if (PCDheader.points !== null) PCDheader.points = parseInt(PCDheader.points[1], 10)

      if (PCDheader.points === null) PCDheader.points = PCDheader.width * PCDheader.height

      if (PCDheader.size !== null) {
        PCDheader.size = PCDheader.size[1].split(' ').map(function (x) {
          return parseInt(x, 10)
        })
      }

      if (PCDheader.count !== null) {
        PCDheader.count = PCDheader.count[1].split(' ').map(function (x) {
          return parseInt(x, 10)
        })
      } else {
        PCDheader.count = []

        for (var i = 0, l = PCDheader.fields.length; i < l; i++) {
          PCDheader.count.push(1)
        }
      }

      PCDheader.offset = {}

      var sizeSum = 0

      for (var i = 0, l = PCDheader.fields.length; i < l; i++) {
        if (PCDheader.data === 'ascii') {
          PCDheader.offset[PCDheader.fields[i]] = i
        } else {
          PCDheader.offset[PCDheader.fields[i]] = sizeSum
          sizeSum += PCDheader.size[i] * PCDheader.count[i]
        }
      }

      // for binary only

      PCDheader.rowSize = sizeSum

      return PCDheader
    }

    var textData = LoaderUtils.decodeText(new Uint8Array(data))

    // parse header (always ascii format)

    var PCDheader = parseHeader(textData)

    // parse data

    var position = []
    var normal = []
    var color = []
    var vertices = []
    var intensities = []

    // ascii

    if (PCDheader.data === 'ascii') {
      var offset = PCDheader.offset
      var pcdData = textData.substr(PCDheader.headerLen)
      var lines = pcdData.split('\n')
      let intensity = 1
      let labelIndex = PCDheader.fields.indexOf('label')
      let imax, imin
      for (var i = 0, l = lines.length; i < l; i++) {
        var line = lines[i].split(' ')
        const _intensity = parseFloat(line[offset.intensity])
        if (isNaN(_intensity)) {
          continue
        }

        if (imax) {
          imax = Math.max(imax, _intensity)
        } else {
          imax = _intensity
        }

        if (imin) {
          imin = Math.min(imin, _intensity)
        } else {
          imin = _intensity
        }
      }

      for (var i = 0, l = lines.length; i < l; i++) {
        if (lines[i] === '') continue

        var line = lines[i].split(' ')

        if (offset.x !== undefined) {
          if (isNaN(parseFloat(line[offset.x]))) {
            position.push(parseFloat(0.0))
            position.push(parseFloat(0.0))
            position.push(parseFloat(0.0))
          } else {
            position.push(parseFloat(line[offset.x]))
            position.push(parseFloat(line[offset.y]))
            position.push(parseFloat(line[offset.z]))
          }
          if (offset.rgb === undefined) {
            if (offset.intensity) {
              intensity = parseFloat(line[offset.intensity])
              if (isNaN(intensity)) {
                intensity = 1
              }
            }
            let z = parseFloat(line[offset.z])
            if (this.renderType === 'intensity') {
              intensity = (intensity - imin) / (imax - imin)
              intensity = (intensity + 0.01) * 8
              color.push(intensity, intensity, intensity)
              // let tmp = colorRender(z, this.renderType);
              // color.push(tmp[0], tmp[1], tmp[2]);
            } else if (this.renderType === 'intensity_customer') {
              intensity = (intensity - imin) / (imax - imin)
              intensity = (intensity + 0.1) * 5
              color.push(intensity, intensity, intensity)
            }
            // else if(this.renderType === "benzRadar") {
            // 	let label = parseInt( line[ offset.label ] );
            // 	while(parseInt(label / 10) != 0) label = parseInt(label / 10);
            // 	switch(label) {
            // 		case 1: color.push(0.0, 0.0, 1.0); break;
            // 		case 2: color.push(0, 190 / 255.0, 70 / 255.0); break;
            // 		case 3: color.push(130 / 255.0, 1.0, 0.0); break;
            // 		// case 202: color.push(1.0, 1.0, 0); break;
            // 		// case 203: color.push(1.0, 0.0, 0.0); break;
            // 		default: color.push(1, 1, 1); break; // 900
            // 	}
            // }
            else {
              // intensity = Math.abs(intensity) > 1 ? intensity / 255.0 : intensity;
              // intensity = (intensity + 0.1) * 5;
              if (offset.intensity) {
                intensity = (intensity - imin) / (imax - imin)
              } else {
                intensity = 1
              }
              let tmp = colorRender(z, this.renderType)
              color.push(tmp[0] * intensity, tmp[1] * intensity, tmp[2] * intensity)
            }
          }
        }

        if (offset.rgb !== undefined) {
          var rgb = parseFloat(line[offset.rgb])
          var r = (rgb >> 16) & 0x0000ff
          var g = (rgb >> 8) & 0x0000ff
          var b = (rgb >> 0) & 0x0000ff
          color.push(r / 255, g / 255, b / 255)
        }

        if (offset.normal_x !== undefined) {
          normal.push(parseFloat(line[offset.normal_x]))
          normal.push(parseFloat(line[offset.normal_y]))
          normal.push(parseFloat(line[offset.normal_z]))
          let vx = parseFloat(line[offset.normal_x])
          let vy = parseFloat(line[offset.normal_y])
          let vz = parseFloat(line[offset.normal_z])
          let vm = Math.sqrt(vx * vx + vy * vy + vz * vz)

          if (vm <= 0.5) {
            // vertices.push( parseFloat( line[ offset.x ] ), parseFloat( line[ offset.y ] ), parseFloat( line[ offset.z ] ) );
            // vertices.push( parseFloat( line[ offset.x ] ), parseFloat( line[ offset.y ] ), parseFloat( line[ offset.z ] ) );
          } else {
            vertices.push(parseFloat(line[offset.x]), parseFloat(line[offset.y]), parseFloat(line[offset.z]))
            vertices.push(
              parseFloat(line[offset.x]) + vx / vm,
              parseFloat(line[offset.y]) + vy / vm,
              parseFloat(line[offset.z]) + vz / vm
            )
          }
        }

        if (offset.h !== undefined) {
          let x = parseFloat(line[offset.x])
          let y = parseFloat(line[offset.y])
          let z = parseFloat(line[offset.z])
          let m = Math.sqrt(x * x + y * y + z * z)
          let vm = parseFloat(line[offset.h])
          vm = vm + m
          let x1 = vm * (x / m)
          let y1 = vm * (y / m)
          let z1 = vm * (z / m)

          if (vm <= 0.5) {
            // vertices.push( parseFloat( line[ offset.x ] ), parseFloat( line[ offset.y ] ), parseFloat( line[ offset.z ] ) );
            // vertices.push( parseFloat( line[ offset.x ] ), parseFloat( line[ offset.y ] ), parseFloat( line[ offset.z ] ) );
          } else {
            vertices.push(parseFloat(line[offset.x]), parseFloat(line[offset.y]), parseFloat(line[offset.z]))
            vertices.push(
              x1,
              y1,
              z1
              // parseFloat(line[offset.x]) + vx / vm,
              // parseFloat(line[offset.y]) + vy / vm,
              // parseFloat(line[offset.z]) + vz / vm
            )
          }
        }

        intensities.push(intensity)
      }
    }

    // binary-compressed

    // normally data in PCD files are organized as array of structures: XYZRGBXYZRGB
    // binary compressed PCD files organize their data as structure of arrays: XXYYZZRGBRGB
    // that requires a totally different parsing approach compared to non-compressed data

    if (PCDheader.data === 'binary_compressed') {
      var sizes = new Uint32Array(data.slice(PCDheader.headerLen, PCDheader.headerLen + 8))
      var compressedSize = sizes[0]
      var decompressedSize = sizes[1]
      var decompressed = decompressLZF(new Uint8Array(data, PCDheader.headerLen + 8, compressedSize), decompressedSize)
      var dataview = new DataView(decompressed.buffer)

      var offset = PCDheader.offset

      let intensity = 1
      let height = 0

      let zIndex = PCDheader.fields.indexOf('z')

      let intensityIndex = PCDheader.fields.indexOf('intensity')

      let labelIndex = PCDheader.fields.indexOf('label')

      let minHeight = 0,
        maxHeight = 0,
        minLabel,
        maxLabel,
        minIntensity = 255,
        maxIntensity = -1
      if (offset.label) {
        maxLabel = dataview.getInt32(PCDheader.points * offset.label, this.littleEndian)
        minLabel = maxLabel
      }
      if (offset.intensity) {
        maxIntensity = dataview.getFloat32(PCDheader.points * offset.intensity, this.littleEndian)
        minIntensity = maxIntensity
      }

      for (let i = 0; i < PCDheader.points; i++) {
        let tmpHeight = dataview.getFloat32(PCDheader.points * offset.z + PCDheader.size[zIndex] * i, this.littleEndian)
        maxHeight = Math.max(maxHeight, tmpHeight)
        minHeight = Math.min(minHeight, tmpHeight)
        if (offset.label) {
          let tmpLabel = dataview.getInt32(
            PCDheader.points * offset.label + PCDheader.size[labelIndex] * i,
            this.littleEndian
          )
          maxLabel = Math.max(maxLabel, tmpLabel)
          minLabel = Math.min(minLabel, tmpLabel)
        }
        if (offset.intensity) {
          let tmpIntensity = dataview.getFloat32(
            PCDheader.points * offset.intensity + PCDheader.size[intensityIndex] * i,
            this.littleEndian
          )
          maxIntensity = Math.max(maxIntensity, tmpIntensity)
          minIntensity = Math.min(minIntensity, tmpIntensity)
        }
      }

      for (let i = 0; i < PCDheader.points; i++) {
        if (offset.x !== undefined) {
          let x = dataview.getFloat32(PCDheader.points * offset.x + PCDheader.size[0] * i, this.littleEndian)
          let y = dataview.getFloat32(PCDheader.points * offset.y + PCDheader.size[1] * i, this.littleEndian)
          let z = dataview.getFloat32(PCDheader.points * offset.z + PCDheader.size[2] * i, this.littleEndian)
          if (isNaN(x) || isNaN(y) || isNaN(z)) {
            x = 0
            y = 0
            z = 0
          }
          position.push(x)
          position.push(y)
          position.push(z)
          height = dataview.getFloat32(PCDheader.points * offset.z + PCDheader.size[2] * i, this.littleEndian)
          if (offset.label && this.renderType !== 'gorgeous') {
            let label = dataview.getInt32(
              PCDheader.points * offset.label + PCDheader.size[labelIndex] * i,
              this.littleEndian
            )

            if (this.renderType == 'deviceId') {
              switch (label) {
                case 26:
                  color.push(0.0, 0.0, 1.0)
                  break
                case 27:
                  color.push(0, 190 / 255.0, 70 / 255.0)
                  break
                case 28:
                  color.push(130 / 255.0, 1.0, 0.0)
                  break
                case 29:
                  color.push(1.0, 1.0, 0)
                  break
                case 30:
                  color.push(1.0, 0.0, 0.0)
                  break
                // default: console.log("label", label); break;
                default:
                  color.push(1, 1, 1)
                  break
              }
            } else if (this.renderType == 'label') {
              let l = (label - minLabel) / (maxLabel - minLabel)
              let tmp = colorRender(l, 'gorgeous')
              color.push(tmp[0], tmp[1], tmp[2])
            } else {
              color.push(label / 30, 1.0, 0.0)
            }
          } else if (offset.rgb === undefined) {
            intensity = 1
            if (offset.intensity) {
              intensity = dataview.getFloat32(
                PCDheader.points * offset.intensity + PCDheader.size[intensityIndex] * i,
                this.littleEndian
              )
              intensity = intensity / (maxIntensity - minIntensity)
              intensity = Math.abs(intensity) > 1 ? intensity / 255.0 : intensity
            }
            if (this.renderType === 'intensity') {
              intensity = (intensity + 0.01) * 8
              color.push(intensity, intensity, intensity)
            } else if (this.renderType === 'intensityGorgeous') {
              // intensity = (intensity + 0.01) * 5
              // color.push(intensity * 1.2, intensity / 1.7, 1 - intensity)
              maxIntensity = 30
              intensity = dataview.getFloat32(
                PCDheader.points * offset.intensity + PCDheader.size[intensityIndex] * i,
                this.littleEndian
              )
              let h = Math.min(1.0, Math.max(0, intensity / maxIntensity)) * 4 + 1
              let rank = Math.floor(h)
              let f = h - rank
              if (rank & 1) f = 1 - f
              let n = 1 - f
              let r, g, b

              if (rank <= 1) {
                r = 0
                g = n
                b = 1
              } else if (rank == 2) {
                r = 0
                g = 1
                b = n
              } else if (rank == 3) {
                r = n
                g = 1
                b = 0
              } else {
                r = 1
                g = n
                b = 0
              }
              color.push(r, g, b)
            } else {
              if (intensity !== 1) intensity = (intensity + 0.01) * 8
              let tmp = colorRender(height, this.renderType)
              color.push(tmp[0] * intensity, tmp[1] * intensity, tmp[2] * intensity)
            }
          }
        }

        if (offset.rgb !== undefined) {
          color.push(dataview.getUint8(PCDheader.points * offset.rgb + PCDheader.size[3] * i + 0) / 255.0)
          color.push(dataview.getUint8(PCDheader.points * offset.rgb + PCDheader.size[3] * i + 1) / 255.0)
          color.push(dataview.getUint8(PCDheader.points * offset.rgb + PCDheader.size[3] * i + 2) / 255.0)
        }

        if (offset.normal_x !== undefined) {
          normal.push(
            dataview.getFloat32(PCDheader.points * offset.normal_x + PCDheader.size[4] * i, this.littleEndian)
          )
          normal.push(
            dataview.getFloat32(PCDheader.points * offset.normal_y + PCDheader.size[5] * i, this.littleEndian)
          )
          normal.push(
            dataview.getFloat32(PCDheader.points * offset.normal_z + PCDheader.size[6] * i, this.littleEndian)
          )
        }

        if (offset.h !== undefined) {
          let x = dataview.getFloat32(PCDheader.points * offset.x + PCDheader.size[0] * i, this.littleEndian)
          let y = dataview.getFloat32(PCDheader.points * offset.y + PCDheader.size[1] * i, this.littleEndian)
          let z = dataview.getFloat32(PCDheader.points * offset.z + PCDheader.size[2] * i, this.littleEndian)
          // let m = Math.sqrt(x * x + y * y + z * z)
          let m = Math.sqrt((x + 1980) * (x + 1980) + (y - 550) * (y - 550) + z * z) /*- 2054.969586 */
          let vm1 = dataview.getFloat32(PCDheader.points * offset.h + PCDheader.size[3] * i, this.littleEndian)
          let vm = vm1 / 6 + m
          let x1 = vm * ((x + 1980) / m) - 1980
          let y1 = vm * ((y - 550) / m) + 550
          let z1 = vm * (z / m)

          if (vm1 <= 0.5) {
          } else {
            vertices.push(x, y, z)
            vertices.push(x1, y1, z1)
          }
        }
      }

      intensities.push(intensity)
    }

    // binary

    if (PCDheader.data === 'binary') {
      var dataview = new DataView(data, PCDheader.headerLen)
      var offset = PCDheader.offset
      let intensity = 1

      for (var i = 0, row = 0; i < PCDheader.points; i++, row += PCDheader.rowSize) {
        if (offset.x !== undefined) {
          position.push(dataview.getFloat32(row + offset.x, this.littleEndian))
          position.push(dataview.getFloat32(row + offset.y, this.littleEndian))
          position.push(dataview.getFloat32(row + offset.z, this.littleEndian))

          if (offset.rgb === undefined) {
            let z = dataview.getFloat32(row + offset.z, this.littleEndian)
            let tmp = colorRender(z, this.renderType)
            color.push(tmp[0], tmp[1], tmp[2])
          }
        }

        if (offset.rgb !== undefined) {
          color.push(dataview.getUint8(row + offset.rgb + 2) / 255.0)
          color.push(dataview.getUint8(row + offset.rgb + 1) / 255.0)
          color.push(dataview.getUint8(row + offset.rgb + 0) / 255.0)
        }

        if (offset.normal_x !== undefined) {
          normal.push(dataview.getFloat32(row + offset.normal_x, this.littleEndian))
          normal.push(dataview.getFloat32(row + offset.normal_y, this.littleEndian))
          normal.push(dataview.getFloat32(row + offset.normal_z, this.littleEndian))
        }
      }

      intensities.push(intensity)
    }

    // build geometry

    var geometry, mesh
    var group = new Group()
    group.name = 'PointsGroup'
    let pointSize = parseFloat(Cookies.get('pointSize'))
    if (!pointSize) pointSize = 2

    if (vertices.length > 0) {
      geometry = this.track(new BufferGeometry())
      geometry.setAttribute('position', new Float32BufferAttribute(vertices, 3))
      let material = this.track(new LineBasicMaterial({ color: 'white' }))
      mesh = this.track(new LineSegments(geometry, material))
      group.add(mesh)

      geometry = this.track(new BufferGeometry())

      if (position.length > 0) geometry.setAttribute('position', new Float32BufferAttribute(position, 3))
      if (normal.length > 0) geometry.setAttribute('normal', new Float32BufferAttribute(normal, 3))
      if (color.length > 0) geometry.setAttribute('color', new Float32BufferAttribute(color, 3))
      if (color.length > 0) geometry.setAttribute('originalColor', new Float32BufferAttribute(color, 3))
      if (intensities.length > 0) geometry.setAttribute('intensities', new Float32BufferAttribute(intensities, 1))

      geometry.computeBoundingSphere()

      material = this.track(new PointsMaterial({ size: pointSize }))
      material.color.setHex(0xffffff)
      material.sizeAttenuation = false
      material.vertexColors = true
      material.depthTest = false
      material.fog = false

      mesh = new Points(geometry, material)
      // var name = url.split('').reverse().join('')
      // name = /([^\/]*)/.exec(name)
      // name = name[1].split('').reverse().join('')
      mesh.name = 'PointsCloud'
      group.add(mesh)
    } else {
      geometry = this.track(new BufferGeometry())

      if (position.length > 0) geometry.setAttribute('position', new Float32BufferAttribute(position, 3))
      if (normal.length > 0) geometry.setAttribute('normal', new Float32BufferAttribute(normal, 3))
      if (color.length > 0) geometry.setAttribute('color', new Float32BufferAttribute(color, 3))
      if (color.length > 0) geometry.setAttribute('originalColor', new Float32BufferAttribute(color, 3))
      if (intensities.length > 0) geometry.setAttribute('intensities', new Float32BufferAttribute(intensities, 1))

      geometry.computeBoundingSphere()

      let material = this.track(new PointsMaterial({ size: pointSize }))
      material.color.setHex(0xffffff)
      material.sizeAttenuation = false
      material.vertexColors = true
      material.depthTest = false
      material.fog = false
      // let canvas = document.createElement("canvas");
      // canvas.width = 100;
      // canvas.height = 100;
      // let context = canvas.getContext("2d");
      // context.fillStyle = "#ffffff";
      // context.arc(50, 50, 45, 0, 2 * Math.PI);
      // context.fill();
      // let texture = new Texture(canvas);
      // texture.needsUpdate = true;
      // var material = new PointsMaterial( { size: 2,  vertexColors: VertexColors, depthTest: false, map: texture0 } );

      // material = new ShaderMaterial( {

      // 	uniforms: {
      // 		color: { value: new Color( 0xffffff ) },
      // 		pointTexture: { value: texture }
      // 	},

      // 	vertexShader: document.getElementById( 'vertexshader' ).textContent,

      // 	fragmentShader: document.getElementById( 'fragmentshader' ).textContent,

      // 	alphaTest: 0.9

      // } );
      // material.sizeAttenuation = false;
      // material.vertexColors = true;
      // material.depthTest = false;
      // material.fog = false;

      // build point cloud

      mesh = new Points(geometry, material)
      // var name = url.split('').reverse().join('')
      // name = /([^\/]*)/.exec(name)
      // name = name[1].split('').reverse().join('')
      mesh.name = 'PointsCloud'
      group.add(mesh)
    }

    return group
  },
})

export { PCDLoader }
